// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Transactions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using System.Collections.Concurrent;

namespace SnapObjects.Data.AspNetCore
{
    /// <summary>
    ///     Specifies that an automatically manage transaction need to be used surrounds execution of the action and the
    ///     action result. The Transaction attributes can be applied to the controller class or the action methods in it.
    /// </summary>
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
    public class TransactionAttribute : ActionFilterAttribute
    {
        private readonly ConcurrentDictionary<string, TransactionScope> _tranScopes;

        /// <summary>
        ///     Initializes a new Instance of the TransactionAttribute class.
        /// </summary>
        public TransactionAttribute()
        {
            this.ScopeTimeout = 0;
            this.CommitPolicy = CommitPolicy.OnActionExecuted;
            this.IsolationLevel = IsolationLevel.Serializable;
            this.AsyncFlowOption = TransactionScopeAsyncFlowOption.Enabled;

            _tranScopes = new ConcurrentDictionary<string, TransactionScope>();
        }

        /// <summary>
        ///     Gets or sets the time (in milliseconds) after which the transaction scope times out and aborts the transaction.
        /// </summary>
        public double ScopeTimeout { get; set; }

        /// <summary>
        ///     Gets or sets when to commit the transaction.
        /// </summary>
        public CommitPolicy CommitPolicy { get; set; }

        /// <summary>
        ///     Gets or sets the isolation level of transaction.
        /// </summary>
        public IsolationLevel IsolationLevel { get; set; }

        /// <summary>
        ///     Gets or sets a TransactionScopeAsyncFlowOption enumeration that describes whether the ambient transaction 
        ///     associated with the transaction scope will flow across thread continuations when using Task or async/await
        ///     .NET async programming patterns.
        /// </summary>
        public TransactionScopeAsyncFlowOption AsyncFlowOption { get; set; }

        /// <summary>
        ///     Called by the ASP.NET MVC framework before the action method is executed.
        /// </summary>
        /// <param name="context">The filter context.</param>
        public override void OnActionExecuting(ActionExecutingContext context)
        {
            base.OnActionExecuting(context);

            var options = new TransactionOptions
            {
                IsolationLevel = this.IsolationLevel,
                Timeout = TimeSpan.FromMilliseconds(this.ScopeTimeout),
            };

            var transactionScope = new TransactionScope(
                TransactionScopeOption.RequiresNew, options, this.AsyncFlowOption);

            if (!String.IsNullOrWhiteSpace(context.HttpContext.TraceIdentifier))
            {
                _tranScopes.TryAdd(context.HttpContext.TraceIdentifier, transactionScope);
            }
        }

        /// <summary>
        ///     Called by the ASP.NET MVC framework after the action method is executed.
        /// </summary>
        /// <param name="context">The filter context.</param>
        public override void OnActionExecuted(ActionExecutedContext context)
        {
            if (this.CommitPolicy == CommitPolicy.OnActionExecuted
                || context.Exception != null || context.Result is IRollbackResult)
            {
                this.ExecuteTransaction(
                    context.HttpContext.TraceIdentifier, context.Result, context.Exception);
            }

            base.OnActionExecuted(context);
        }

        /// <summary>
        ///     Called by the ASP.NET MVC framework after the action result is executed.
        /// </summary>
        /// <param name="context">The filter context.</param>
        public override void OnResultExecuted(ResultExecutedContext context)
        {
            if (this.CommitPolicy == CommitPolicy.OnResultExecuted && 
                !(context.Result is IRollbackResult))
            {
                this.ExecuteTransaction(
                    context.HttpContext.TraceIdentifier, context.Result, context.Exception);
            }

            base.OnResultExecuted(context);
        }

        private void ExecuteTransaction(
            string scopeKey, IActionResult actionResult, Exception exception)
        {
            if (_tranScopes.TryGetValue(scopeKey, out var transScope))
            {
                try
                {
                    if (exception != null || 
                        (actionResult is IRollbackResult && Transaction.Current != null))
                    {
                        Transaction.Current.Rollback();
                    }
                    else
                    {
                        transScope.Complete();
                    }
                }
                finally
                {
                    transScope.Dispose();

                    _tranScopes.TryRemove(scopeKey, out transScope);
                }
            }
        }
    }
}
